#!/usr/bin/env ruby

LKP_SRC = ENV['LKP_SRC'] || File.dirname(File.dirname(File.realpath($PROGRAM_NAME)))

require "#{LKP_SRC}/lib/statistics"
require "#{LKP_SRC}/lib/string_ext"
require "#{LKP_SRC}/lib/array_ext"
require "#{LKP_SRC}/lib/tests/stats"

stats = LKP::Stats.new

test = nil
mqueue_speed = {}
tests_stats = Hash.new { |h, k| h[k] = {} }

def futex_stat_result(futex, result, stats)
  if futex['argument'] && !(futex['argument'].include? 'none')
    stats.add "futex.#{futex['subtest']}.#{futex['argument']}", result
  else
    stats.add "futex.#{futex['subtest']}", result
  end

  futex['argument'] = nil
end

def futex_stat(line, futex, stats)
  case line
  when /^(# )*(futex.+): /
    futex['subtest'] = $2
  when /^(# )*\s+Arguments: (.+)/
    futex['argument'] = $2
  when /Result\:\s+(PASS|FAIL)/,
       /^selftests\:.*(PASS|FAIL)/
    futex_stat_result(futex, $1, stats)
  when /^(# )*(ok|not ok) \d+/
    futex_stat_result(futex, $2, stats)
  end
end

class Stater
  def initialize(test, test_script)
    @test = test
    @test_script = test_script

    @test_prefix = "#{@test}.#{@test_script}"
  end

  def stat(line, stats)
    case line
    when /^(ok|not ok) \d+ selftests: (#{@test}): (#{@test_script})/
      # not ok 46 selftests: net: vrf_route_leaking.sh # exit=1
      # ok 1 selftests: vm: run_vmtests
      stats.add @test_prefix.to_s, $1
    end
  end
end

class NetStater < Stater
  def stat(line, stats)
    case line
    when /^#\s+(IPv.+)/
      # IPv4 (sym route): VRF ICMP error route lookup traceroute
      @test_group = $1
    when /^#\s+TEST:\s+(.+)\s+\[( OK |FAIL|SKIP)\]/
      # TEST: Basic IPv4 connectivity                                       [ OK ]
      stats.add "#{@test_prefix}.#{@test_group}.#{$1}", $2
    when /^#\s+(SKIP|FAIL)/
      # SKIP: Could not run IPV4 test without traceroute
      stats.add "#{@test_prefix}.#{@test_group}", $1
    else
      super(line, stats)
    end
  end
end

class VmStater < Stater
  def stat(line, stats)
    case line
    when /^#\s+running\s+(.+)/
      # running hugepage-shm
      @test_case = $1
    when /^#\s+\[(PASS|FAIL|ignored_by_lkp)\]/
      # rli9 FIXME why below test has both ignored and pass result
      #
      # running virtual address 128TB switch test
      # -----------------------------
      # [ignored_by_lkp]
      # [PASS]
      stats.add "#{@test_prefix}.#{@test_case}", $1 if @test_case
      @test_case = nil
    else
      super(line, stats)
    end
  end
end

def memory_hotplug_stat(line, memory_hotplug, stats)
  case line
  when /selftests.*: (.+\.sh)/
    memory_hotplug['subtest'] = $1
  when /^\.\/(.+\.sh).+selftests: memory-hotplug/ # for kernel < v4.18-rc1
    memory_hotplug['subtest'] = $1
  when /^*selftests.*: memory-hotplug \[FAIL\]/
    stats.add "memory-hotplug.#{memory_hotplug['subtest']}", 'fail'
    memory_hotplug['subtest'] = nil
  when %r{make: Leaving directory .*/(.*)'}
    stats.add "memory-hotplug.#{memory_hotplug['subtest']}", 'pass' if memory_hotplug['subtest']
  end
end

# for kernel < v4.18-rc1
def mount_stat(line, mount, stats)
  case line
  when %r{if .* then ./(.*) ; else echo}
    mount['subtest'] = $1
  when /^WARN\: No \/proc\/self\/uid_map exist, test skipped/
    stats.add "mount.#{mount['subtest']}", 'skip'
    mount['subtest'] = nil
  when /(^(MS.+|Default.+) malfunctions$)|(^Mount flags unexpectedly changed after remount$)/
    stats.add "mount.#{mount['subtest']}", 'fail'
    mount['subtest'] = nil
  when %r{make: Leaving directory .*/(.*)'}
    stats.add "mount.#{mount['subtest']}", 'pass' if mount['subtest']
  end
end

def x86_stat(line, _x86, stats)
  case line
  when /can not run MPX tests/
    @pmx = 'skip'
  when /^*selftests.*: (.*) .*(PASS|FAIL)/
    test_script = $1.strip
    result = $2
    if test_script =~ /mpx-mini-test/ && @pmx
      stats.add "x86.#{test_script}", @pmx
    else
      stats.add "x86.#{test_script}", result
      @pmx = nil
    end
  end
end

def resctrl_stat(line, _resctrl, stats)
  stats.add "resctrl.#{$1.strip}", 'fail' if line =~ /^not ok\s+(.*)/
end

def subtest_stat(test, test_script, subtest, result, stats)
  subtest_case_name = subtest.strip.gsub(/[-\t\r\n\f]/, '_')

  stats.add "#{test}.#{test_script}.#{subtest_case_name}", result
end

def detail_stat(test, test_script, result, stats)
  test_script = test_script.strip.gsub(/[-\t\r\n\f]/, '_')

  stats.add "#{test}.#{test_script}", result
end

while (line = STDIN.gets)
  line = line.resolve_invalid_bytes

  case line
  when /^# selftests: (net): (vrf_route_leaking.sh)/
    stater = NetStater.new($1, $2)
  when /^# selftests: (vm): (.+)/
    # selftests: vm: run_vmtests
    stater = VmStater.new($1, $2)
  when /^# selftests: net: (.*)/
    test_script = $1
    stater = nil
  when /^# selftests: (.+): (.+)/
    stater = nil
  else
    if stater
      stater.stat(line, stats)
      next
    end
  end

  case line
  when %r{make: Entering directory .*/(.*)'}
    test = Regexp.last_match[1]
    if test == 'kmod'
      last_subtest_case_name = nil
      subtest_case_name = nil
      subtest_case_result = nil
      all_subtest_case_skip = true
    end
  when /^ok kernel supports resctrl filesystem/
    test = 'resctrl'
  when %r{make: Leaving directory .*/(.*)'}
    if test == 'memory-hotplug'
      memory_hotplug_stat(line, tests_stats['memory-hotplug'], stats)
    elsif test == 'mount'
      mount_stat(line, tests_stats['mount'], stats)
    else
      # rli9 FIXME: consider the input has messed text like Entering doesn't match with Leaving
      test = nil
      test_script = nil
    end
  when /^# \[       (OK|FAIL|SKIP) \] (.*)/
    next unless test == 'net'

    detail_stat('net', $2, $1, stats)
  when /^(ion_test.sh: .*) - \[(PASS|FAIL|SKIP)\]$/
    next unless test == 'android'

    detail_stat('android', $1, $2, stats)
  when /^(ok|fail|skip) \d+ (Test .*)/
    next unless test == 'breakpoints'

    detail_stat('breakpoints', $2, $1, stats)
  when /^# \[RUN\].*(Tests with uid .*) +++/
    next unless test == 'capabilities'

    test_script = $1.strip
  when /^Pass (\d+) Fail (\d+) Xfail (\d+) Xpass (\d+) Skip (\d+) Error (\d+)/
    next unless test == 'capabilities'

    result = 'skip'
    result = 'fail' if $2 != '0' || $3 != '0' || $6 != '0'
    result = 'pass' if $2 == '0' && $3 == '0' && $6 == '0'

    stats.add "#{test}.#{test_script}", result
  when /^(Check .*)... \[(OK|FAIL|SKIP)\]/
    next unless test == 'exec'
    next if $1 =~ /execveat/

    detail_stat('exec', $1, $2, stats)
  when /^(PASS|FAIL|SKIP): (.*)/
    next unless test == 'ia64'

    detail_stat('ia64', $2, $1, stats)
  when /^# selftests: futex: (.*)/
    test_script = $1
  when /^# selftests: netfilter: (.*)/
    test_script = $1
  when /^# (PASS|FAIL|SKIP): (.*)/
    next unless %w(net netfilter).include? test

    subtest_stat(test, test_script, $2, $1, stats) unless %w(fib_nexthops.sh).include? test_script
  when /^# (Single|Multipath|Single|Admin|Local|Single|FIB|IPv4|IPv6|Basic|Legacy|Routing) (.*)/
    next unless %w(icmp_redirect.sh).include? test_script

    subtest_case_name1 = $1 + ' ' + $2
  when /^# TEST SECTION: (.*)/
    next unless %w(fib-onlink-tests.sh fib_rule_tests.sh).include? test_script

    subtest_case_name1 = $1
  when /^# TEST SUBSECTION: (.*)/
    next unless test_script == 'fib-onlink-tests.sh'

    subtest_case_name2 = $1
  when /^#     TEST: (.*) \[ ?(OK|FAIL|SKIP) ?\]$/
    next unless %w(fib-onlink-tests.sh fib_rule_tests.sh).include? test_script

    subtest_case_name = subtest_case_name1 + '.' + $1
    subtest_case_name = subtest_case_name1 + '.' + subtest_case_name2 + '.' + $1 if test_script == 'fib-onlink-tests.sh'
    subtest_stat('net', test_script, subtest_case_name, $2, stats)
  when /^# TEST: (.*) \[ ?(PASS|OK|FAIL|SKIP) ?\]/
    next unless %w(pmtu.sh icmp_redirect.sh ip6_gre_headroom.sh).include? test_script

    subtest_case_name = $1
    subtest_case_name = subtest_case_name1 + '.' + $1 if %w(icmp_redirect.sh).include? test_script
    subtest_stat('net', test_script, subtest_case_name, $2, stats)
  when /^#   (net|port)(.*)\[( OK|FAIL|SKIP)/
    next unless test_script == 'nft_concat_range.sh'

    subtest_case_name = subtest_case_name1 + '.' + $1 + $2
    subtest_stat('netfilter', test_script, subtest_case_name, $3, stats)
  when /^# (UDP|TCP|DCCP) (.*) \.\.\. (pass|fail|skip)/
    next unless test_script == 'reuseport_addr_any.sh'

    subtest_case_name = $1 + ' ' + $2
    subtest_stat('net', test_script, subtest_case_name, $3, stats)
  when /^# selftests: kmod: (.*)/
    test_script = $1
  when /^# Running test: (kmod_test.*) - run/, /^# Test completed/
    next unless test == 'kmod'

    last_subtest_case_name = subtest_case_name
    # '# Test completed' marks the whole kmod tests finished, where no $1
    subtest_case_name = $1 unless $1.nil?
    # a test may run several times, regard them as one test
    # # Running test: kmod_test_0005 - run #1
    # # kmod_test_0005: OK! - loading kmod test
    # # kmod_test_0005: OK! - Return value: 0 (SUCCESS), expected SUCCESS
    # # Tue Sep 15 17:57:54 UTC 2020
    # # Running test: kmod_test_0005 - run #2
    # # kmod_test_0005: OK! - loading kmod test
    # # kmod_test_0005: OK! - Return value: 0 (SUCCESS), expected SUCCESS
    # need notice when meet 'Test completed', when $1 is nil, so
    # subtest_case_name doesn't change. below "&& !$1.nil?" is used to
    # avoid missing last subtest handling
    next if subtest_case_name == last_subtest_case_name && !$1.nil?

    # when starting 'Running' first test, there is no last_subtest_case_name
    next if last_subtest_case_name.nil?

    # subtest_case_result and all_subtest_case_skip are still corresponding to last_subtest_case_name here.
    # need calibrate result in one situation when meet -
    # (1) no 'FAIL'
    # (2) not all subtests are 'SKIP'
    # whatever the subtest_case_result is 'OK' or 'SKIP' here, regard whole subtest 'pass'
    subtest_case_result = 'OK' if subtest_case_result != 'FAIL' && !all_subtest_case_skip
    subtest_stat('kmod', test_script, last_subtest_case_name, subtest_case_result, stats)
    # reset subtest_case_result and all_subtest_case_skip for new subtest
    subtest_case_result = nil
    all_subtest_case_skip = true
  when /^# (kmod_test.*|kmod_check_visibility): (OK|FAIL|SKIP)/
    next unless test == 'kmod'

    # if any single test fails, regard the whole subtest fail
    next if subtest_case_result == 'FAIL'

    subtest_case_result = $2
    # if results are OK+SKIP, will regard as 'pass'.
    # when all subtest results are 'SKIP', regard the subtest as 'skip'
    next unless all_subtest_case_skip

    all_subtest_case_skip = subtest_case_result == 'SKIP'
  when /# selftests: membarrier: (.*)/
    test_script = $1
  when /# selftests: pidfd: (.*)/
    test_script = $1
  when /# (ok|fail|skip) \d+ (.*):(.*)/
    if test == 'pidfd'
      subtest_stat('pidfd', test_script, $2, $1, stats) unless test_script == 'pidfd_test'
    elsif test == 'timens'
      subtest_case_name = $2 + ':' + $3
      subtest_stat('timens', test_script, subtest_case_name, $1, stats)
    end
  when /# selftests: pstore: (.*)/
    test_script = $1
  when /# selftests: timens: (.*)/
    test_script = $1
  when /# (ok|fail|skip) \d+ (.*)/
    # futex_stat(line, tests_stats['futex'], stats) if test == 'futex'
    subtest_stat('timens', test_script, $2, $1, stats) if test == 'timens'
  when /^# selftests: timers: (.*)/
    test_script = $1
  when /^# (.+\w)(\.\.\.)?\s+\[(OK|FAIL|SKIP|UNSUPPORTED)\]/,
       /^# ([^:]+\w)(\s?:.+)\[(OK|FAIL|SKIP|UNSUPPORTED)\]/
    # Check itimer virtual... [OK]
    # Nanosleep CLOCK_MONOTONIC                 [OK]
    # Mqueue latency :                          [OK]
    # Testing consistency with 8 threads for 30 seconds: [OK]
    # Estimating clock drift: 0.0(est) 0.0(act)     [OK]
    # CLOCK_TAI              RELTIME ONE-SHOT count:                   1 : [OK]
    next unless test == 'timers'

    subtest_stat('timers', test_script, $1, $3, stats)
  when /^*selftests.*: (.*) .*(\[|\[ )(PASS|FAIL|SKIP)/
    next if test == 'bpf'

    if test == 'futex'
      # futex_stat(line, tests_stats['futex'], stats)
    elsif test == 'memory-hotplug'
      memory_hotplug_stat(line, tests_stats['memory-hotplug'], stats)
    elsif test == 'x86'
      x86_stat(line, tests_stats['x86'], stats)
    else
      stats.add "#{test}.#{Regexp.last_match[1].strip}", Regexp.last_match[3]
    end
  when /^# selftests: livepatch: (.*)/
    test_script = $1
  when /^# TEST: (.*) \.\.\. (ok|fail|skip)/
    next unless test == 'livepatch'

    subtest_stat('livepatch', test_script, $1, $2, stats)
  when /^# selftests: bpf: (.*)/
    test_script = $1

  when /^# Running kernel configuration test \d+ -- (.*)/
    next unless test == 'firmware'

    test_script = $1
  when /^# Testing with the (file .*)\.\.\.$/
    next unless test == 'firmware'

    subtest_case_name1 = $1
  when /^# (.*): ?(PASS|OK|FAIL|SKIP|Pass|Fail|Skip)/
    if test == 'firmware'
      subtest_case_name = subtest_case_name1 + '.' + $1
      subtest_stat('firmware', test_script, subtest_case_name, $2, stats)
    end
  when /^# Test case: (.*) .. \[(PASS|FAIL|SKIP)\]/
    next unless %w(test_sysctl test_sock test_sock_addr.sh).include? test_script

  when /^# Test   \d+: (.*) ... (PASS|Fail).*/
    next unless test_script == 'test_align'

  when /^# (.*) \.\.\. (ok|fail|skip)/
    subtest_stat('pstore', test_script, $1, $2, stats) if test == 'pstore'
  when /^# selftests: mqueue: (.*)/
    test_script = $1
  when /^# (.*):.*(PASS|FAIL|SKIP)/
    next unless test == 'mqueue'

    subtest_stat('mqueue', test_script, $1, $2, stats)
  when /Test #([1-9].*):/
    next unless test == 'mqueue'

    mqueue_test = Regexp.last_match[1]
  when /(Send|Recv) msg:/
    io = Regexp.last_match[1]
  when %r{(\d+) nsec/msg}
    mqueue_speed[mqueue_test + '.' + io] = Regexp.last_match[1].to_i
  when /: recipe for target.+failed$/, /^make: \*\*\* (.*) (Error \d+|Stop\.)$/
    next unless test

    # do not count make fail in nr_test, which is for sub test number
    stats.add test, 'make_fail'
  when /^ignored_by_lkp (.*) test/
    stats.add $1, 'ignored_by_lkp'
  when /^(ok|not ok) \d+ selftests: (\S*): (\S*)( # SKIP)?/
    next if %w(memory-hotplug).include? test

    test = $2
    test_script = $3
    result = $4 =~ /SKIP/ ? 'skip' : 'fail'
    result = 'pass' if $1 == 'ok'

    stats.add "#{test}.#{test_script}", result unless %w(pmtu.sh fib_rule_tests.sh reuseport_addr_any.sh traceroute.sh altnames.sh icmp_redirect.sh ip6_gre_headroom.sh fib-onlink-tests.sh mq_open_tests pidfd_fdinfo_test pidfd_open_test pidfd_poll_test pidfd_wait pstore_tests pstore_post_reboot_tests kmod.sh).include? test_script
  when /^# (.*) ?\[ (OK|FAIL|SKIP) \]/
    next unless test == 'mptcp'

    # $1 is "ns1 MPTCP -> ns1 (10.0.1.1:10000      ) MPTCP   (duration  1089ms)"
    # test_script is "ns1 MPTCP -> ns1 (10.0.1.1:10000      ) MPTCP"
    # $2 is "OK", $2 should be saved before changing $1
    result = $2
    test_script = $1.gsub(/\(duration(.*)\)/, '')
    detail_stat('mptcp', test_script, result, stats)
  when /^# TEST: (.*)/
    next unless test_script == 'nft_concat_range.sh'

    subtest_case_name1 = $1
  else
    if test == 'futex'
      # futex_stat(line, tests_stats['futex'], stats)
    elsif test == 'memory-hotplug'
      memory_hotplug_stat(line, tests_stats['memory-hotplug'], stats)
    elsif test == 'mount'
      mount_stat(line, tests_stats['mount'], stats)
    elsif test == 'x86'
      x86_stat(line, tests_stats['x86'], stats)
    elsif test == 'resctrl'
      resctrl_stat(line, tests_stats['resctrl'], stats)
    end
  end
end

stats.add 'mqueue.nsec_per_msg', mqueue_speed.values.average.to_i unless mqueue_speed.empty?

stats.dump('ok' => 'pass', 'not_ok' => 'fail')
